#!/usr/bin/env python
# -*- coding: utf-8 -*-
from math import fabs
from DrissionPage import ChromiumPage
from DrissionPage.errors import ElementNotFoundError
import time
import random
import json
import os
import traceback

TEMP_DIR = 'temp'
PROCESSED_POSTS_FILE = os.path.join(TEMP_DIR, "processed_posts.json") # 定义存储已处理帖子链接的文件名

from PyQt6.QtCore import QObject, pyqtSignal # 导入 QObject 和 pyqtSignal

class XiaohongshuBot(QObject): # 继承 QObject
    """小红书自动化机器人，用于自动登录和评论"""
    log_signal = pyqtSignal(str, int) # 定义日志信号，增加 port

    def __init__(self, user_data_dir=None, port=None, parent=None): # 添加 port 参数
        super().__init__(parent) # 调用父类构造函数
        self.port = port
        """初始化浏览器对象

        Args:
            user_data_dir: 浏览器用户数据保存目录，用于保存登录状态
            port: 浏览器调试端口
            parent: 父对象，用于信号机制
        """
        from DrissionPage import ChromiumOptions
        co = ChromiumOptions()

        if user_data_dir:
            # 设置自定义用户数据目录
            co.set_user_data_path(user_data_dir)

        if port:
            # 设置独立的调试端口
            co.set_local_port(port)
            self._log(f"使用端口 {port} 启动浏览器实例。")
        else:
            self._log("未指定端口，使用默认端口或自动选择端口启动浏览器实例。")

        # 使用配置好的 ChromiumOptions 创建页面对象
        self.page = ChromiumPage(addr_or_opts=co)

        self.is_stopped = False
        self.user_info = None
        self.user_data_dir = user_data_dir
        self.port = port  # 保存端口信息到实例变量
        self.processed_hrefs_file = PROCESSED_POSTS_FILE

    def get_port(self):
        """获取当前浏览器实例的端口号"""
        return self.port

    def _log(self, message):
        """发送日志信号并打印到控制台"""
        print(f"[XiaohongshuBot][port={self.port}] {message}")
        self.log_signal.emit(message, self.port if self.port else -1)


    def quit(self):
        """关闭浏览器页面"""
        self.is_stopped = True # 设置停止标记
        self._log("尝试关闭浏览器...")
        try:
            if self.page:
                self.page.quit()
                self._log("浏览器已关闭。")
            else:
                self._log("浏览器页面对象不存在，无法关闭。")
        except Exception as e:
            self._log(f"关闭浏览器时出错: {e}")

    def random_delay(self, min_seconds=2, max_seconds=50):
        """生成一个随机延迟时间"""
        delay = random.uniform(min_seconds, max_seconds)
        self._log(f"随机等待 {delay:.2f} 秒...")
        # 使用小间隔，以便能够及时响应停止命令
        for _ in range(int(delay * 10)):
            if self.is_stopped:
                break
            time.sleep(0.1)

    def _load_processed_hrefs(self):
        """从本地 JSON 文件加载已处理的帖子 href 集合，只保留最后100条记录"""
        if hasattr(self, '_processed_hrefs_cache') and self._processed_hrefs_cache is not None:
            return self._processed_hrefs_cache
            
        try:
            if os.path.exists(self.processed_hrefs_file):
                with open(self.processed_hrefs_file, 'r', encoding='utf-8') as f:
                    content = f.read()
                    if not content.strip(): # 检查文件是否为空或只包含空白字符
                        self._log(f"{self.processed_hrefs_file} 文件为空，将创建新的记录。")
                        href_list = []
                    else:
                        try:
                            href_list = json.loads(content)
                        except json.JSONDecodeError as decode_err:
                            self._log(f"解析 {self.processed_hrefs_file} JSON内容时出错: {decode_err}，将使用空列表。")
                            href_list = []

                    if not isinstance(href_list, list):
                        self._log(f"{self.processed_hrefs_file} 内容格式不正确（非列表），将使用空列表。")
                        href_list = []

                    # 只保留最后100条记录
                    if len(href_list) > 100:
                        href_list = href_list[-100:]
                        self._log(f"从 {self.processed_hrefs_file} 加载并清理了记录，保留最后100条。")
                    else:
                        self._log(f"从 {self.processed_hrefs_file} 加载了 {len(href_list)} 条已处理记录。")
                    self._processed_hrefs_cache = set(href_list)
                    return self._processed_hrefs_cache
            else:
                self._log(f"未找到 {self.processed_hrefs_file} 文件，将创建新的记录。")
                self._processed_hrefs_cache = set()
                return self._processed_hrefs_cache
        except json.JSONDecodeError as e: # 捕获json.loads()的错误
            self._log(f"加载 {self.processed_hrefs_file} 文件时 JSON 解析出错: {e}，将使用空集合。")
            self._processed_hrefs_cache = set()
            return self._processed_hrefs_cache
        except IOError as e:
            self._log(f"加载 {self.processed_hrefs_file} 文件时 IO 出错: {e}，将使用空集合。")
            self._processed_hrefs_cache = set()
            return self._processed_hrefs_cache
        except Exception as e: # 捕获其他未知错误
            self._log(f"加载 {self.processed_hrefs_file} 文件时发生未知错误: {e}，将使用空集合。")
            traceback.print_exc()
            self._processed_hrefs_cache = set()
            return self._processed_hrefs_cache

    def _save_processed_hrefs(self, href_set):
        """将已处理的帖子 href 集合保存到本地 JSON 文件，只保留最后100条记录"""
        try:
            # Convert set to list for sorting and slicing
            href_list = sorted(list(href_set)) # 确保从set转换为list
            
            # 只保留最后100条记录
            if len(href_list) > 100:
                href_list = href_list[-100:]
                self._log(f"清理记录，保留最后100条。")
            
            # Update the in-memory cache (as a set for efficient lookups)
            self._processed_hrefs_cache = set(href_list) 
            
            # Always write to the file to ensure data persistence
            with open(self.processed_hrefs_file, 'w', encoding='utf-8') as f:
                # Use separators for compact JSON, indent=None for no newlines
                json.dump(href_list, f, ensure_ascii=False, separators=(',', ':')) 
                self._log(f"已将 {len(href_list)} 条处理记录保存到 {self.processed_hrefs_file}。")
        except IOError as e:
            self._log(f"保存 {self.processed_hrefs_file} 文件时 IO 出错: {e}")
        except Exception as e: # 捕获任何其他意外错误
            self._log(f"保存 {self.processed_hrefs_file} 文件时发生未知错误: {e}")
            traceback.print_exc()

    def _sort_by_newest(self):
        """尝试点击筛选按钮并选择最新排序"""
        try:
            # 检查是否有下拉菜单项且包含'综合'
            filter= self.page.ele('.filter', timeout=5)
            if filter.text in ("综合","最新","最热"):
                 filter.click()
                 dropdown_items= self.page.eles('.dropdown-items', timeout=5)
                 for item in dropdown_items:
                    try:
                       item.ele('综合',timeout=1).check()
                    except:
                       continue
                 self.random_delay(2, 5)
                 
                 filter.click()
                 dropdown_items= self.page.eles('.dropdown-items', timeout=5)
                 for item in dropdown_items:
                    try:
                       item.ele('最新',timeout=1).check()
                    except:
                       continue
                 self.random_delay(2, 5)
                 return True
            
            # 原逻辑作为后备方案
            filter_panel = self.page.ele('.filter-panel', timeout=1)
            if filter_panel:
                self._log("筛选面板已打开，直接尝试点击'最新'选项...")
            else:
                self._log("筛选面板未打开或不可见，尝试点击筛选按钮...")
                filter_btn = self.page.ele('.filter', timeout=5)
                if filter_btn:
                    self._log("找到筛选按钮，尝试点击...")
                else:
                    self._log("未找到筛选按钮，尝试通过文字定位...")
                    filter_btn = self.page.ele('已筛选', timeout=5)
                
                if not filter_btn:
                    self._log("错误：未能定位到筛选按钮。")
                    return False

                try:

                    # 尝试使用 JavaScript 点击，忽略可见性和启用状态
                    filter_btn.click(by_js=True)

                    self._log("点击筛选按钮操作已执行，等待下拉菜单...")
                    self.random_delay(2, 5) # 等待下拉菜单出现
                except Exception as click_err:
                    self._log(f"错误：点击筛选按钮时发生异常: {click_err}")
                    traceback.print_exc() # 打印详细的错误堆栈
                    return False # 点击失败，返回 False

            # 点击"最新"选项
            try:
                self._log("尝试直接查找并点击'最新'选项...")
                # 确保 filter-panel 存在才查找内部的 '最新'
                target_panel = self.page.ele('.filter-panel', timeout=5) 
                if not target_panel:
                    self._log("错误：未能找到 .filter-panel。")
                    return False
                target_panel.ele('最新', timeout=5).click() # 直接查找并点击
                self._log("已点击'最新'，等待结果刷新...")
                self.random_delay(3, 5) # 等待页面内容根据新排序刷新
                return True # 点击成功，返回 True
            except ElementNotFoundError:
                self._log("错误：查找'最新'选项超时或未找到。")
                return False
            except Exception as latest_click_err:
                self._log(f"错误：点击'最新'选项时发生异常: {latest_click_err}")
                traceback.print_exc()
                # 如果点击失败，可能需要点击其他地方关闭菜单，避免遮挡
                try: self.page.click(10, 10) # 点击页面左上角尝试关闭
                except: pass
                return False
        
        except Exception as filter_e:
            self._log(f"执行排序到最新时发生错误: {filter_e}")
            return False



    def check_login(self,tab, monitor_timeout=180, timeout=5):
        """持续监控登录状态直到成功或超时。

        Args:
            monitor_timeout (int): 持续监控的总超时时间（秒）。
            timeout (int): 单次API监听的超时时间（秒）。

        Returns:
            tuple: (是否已登录, 用户信息 dict or None)
        """
        self._log(f"开始检查登录状态，监控超时时间：{monitor_timeout}秒")

        # 检查缓存的用户信息
        if self.user_info and self.user_info.get('is_logged', False):
            self._log(f"用户已登录：{self.user_info.get('nickname')}")
            return True, self.user_info

        # 检查页面标题
        try:
            page_title = self.page.title
            if '小红书' in page_title and '登录' not in page_title:
                self._log(f"页面标题 '{page_title}' 显示可能已登录，尝试通过API确认")
        except Exception as e:
            self._log(f"获取页面标题时出错：{e}")

        # 开始持续监控登录状态
        start_time = time.time()
        check_count = 0
        self._log(f"开始API监控登录状态，总超时: {monitor_timeout}秒")

        # 监控循环
        while time.time() - start_time < monitor_timeout:
            # 检查停止信号
            if self.is_stopped:
                self._log("收到停止信号，中断登录监控")
                return False, None

            # 每5次检查更新一次状态日志
            check_count += 1
            elapsed_time = time.time() - start_time
            if check_count % 5 == 0:
                self._log(f"监控中...已等待{int(elapsed_time)}秒，剩余{int(monitor_timeout - elapsed_time)}秒")

            # 执行API监听检查
            try:
                packet = tab.listen.wait(timeout=timeout)

                # 处理有效响应
                if packet and hasattr(packet, 'is_failed') and not packet.is_failed and packet.response:
                    data = packet.response.body

                    # 详细记录API返回值
                    try:
                        import json
                        if isinstance(data, dict):
                            self._log(f"API返回详细数据: {json.dumps(data, ensure_ascii=False, indent=2)}")
                        else:
                            self._log(f"API返回数据类型: {type(data).__name__}")
                            self._log(f"API返回内容: {str(data)[:1000]}" + ("..." if len(str(data)) > 1000 else ""))
                    except Exception as log_err:
                        self._log(f"记录API返回值时出错: {log_err}")

                    # 检查是否为有效的登录响应
                    if isinstance(data, dict):
                        self._log(f"API返回状态: success={data.get('success', False)}, 包含data字段: {'data' in data}")
                        if data.get('success') and 'data' in data:
                            user_data = data['data']
                            nickname = user_data.get('nickname')

                            # 记录用户数据字段
                            self._log(f"用户数据字段: {', '.join(user_data.keys() if isinstance(user_data, dict) else ['非字典类型'])}")

                            if nickname:
                                # 构建用户信息
                                user_info = {
                                    'nickname': nickname,
                                    'red_id': user_data.get('red_id', ''),
                                    'user_id': user_data.get('user_id', ''),
                                    'avatar': user_data.get('avatar', user_data.get('imageb', '')),
                                    'is_logged': True
                                }
                                self._log(f"登录成功：{nickname}, red_id={user_data.get('red_id', '无')}, user_id={user_data.get('user_id', '无')}")
                                self.user_info = user_info
                                return True, user_info
                            else:
                                self._log("未找到nickname字段，登录验证失败")
                    else:
                        self._log(f"API返回非字典类型数据，无法解析登录状态")
                elif packet:
                    self._log(f"收到API响应但格式不符合预期: {packet}")
                else:
                    self._log("API监听超时，未收到响应")
            except Exception as e:
                self._log(f"API监听出错: {str(e)[:100]}")
                import traceback
                self._log(f"错误详情: {traceback.format_exc()[:500]}...")
                # 出现错误直接终止监听并返回失败
                self._log("由于API监听出错，终止登录监控")
                return False, None

            # 等待一段时间再继续检查
            for _ in range(4):  # 分段等待2秒，便于响应停止信号
                if self.is_stopped:
                    return False, None
                time.sleep(0.5)

        # 超时处理
        self._log(f"登录监控超时（{monitor_timeout}秒），未能确认登录状态")
        return False, None

    def login(self, monitor_timeout=180):
        """打开小红书主页并持续监控登录状态。
        登录成功后会自动根据user_id保存或更新用户信息和端口到accounts.json文件。

        Args:
            monitor_timeout (int): 监控登录状态的总超时时间（秒）。
            open_browser (bool): 是否打开浏览器，如果为False则只监控登录状态。

        Returns:
            tuple: (是否登录成功, 用户信息 dict or None)
        """
        try:
            # 使用当前页面
            tab = self.page

            self._log("正在打开小红书主页以进行登录...")
            tab.listen.start('/api/sns/web/v2/user/me')
            tab.get("https://www.xiaohongshu.com/")
            self._log("主页已打开，现在开始持续监控登录状态...")

            # 调用 check_login 进行持续监控
            is_logged_in, user_info = self.check_login(tab,monitor_timeout=monitor_timeout)

            if is_logged_in:
                self._log("登录监控成功！")
                # 确保用户信息包含端口号
                if user_info:
                    user_info['port'] = self.port  # 添加或更新端口信息
                else:  # 万一user_info是None，但is_logged_in为True的情况（理论上不应发生）
                    user_info = {'port': self.port, 'is_logged': True}
                # 只返回用户信息，不再直接保存账号数据
                return True, user_info
            else:
                self._log("登录监控失败或超时。")
                return False, None

        except Exception as e:
            self._log(f"登录过程中出错: {e}")
            traceback.print_exc()
            return False, None

    def search_and_comment(self, keyword, comment_texts, max_posts=5, thread_instance=None):
        """搜索小红书帖子并评论

        Args:
            keyword: 搜索关键词
            comment_texts: 评论内容列表，随机选择
            max_posts: 最多评论多少个帖子
            thread_instance: 调用此方法的 AccountThread 实例，用于发送信号

        Returns:
            bool: 是否成功完成评论
        """
        self._log(f"开始搜索关键词: {keyword}")

        try:
            # 访问小红书搜索结果页，按最新排序
            search_url = f"https://www.xiaohongshu.com/search_result/?keyword={keyword}&source=web_search_result_notes&sort=newest"
            self._log(f"访问搜索结果页: {search_url}")
            self.page.get(search_url)
            self.random_delay(2, 4) # 访问页面后等待

            # 定位帖子列表容器 - 移动到循环内部
            post_item_selector = "css:.note-item"

            processed_hrefs = self._load_processed_hrefs() # 从文件加载
            commented_count = 0 # 评论计数器
            posts_to_check = 3 # 每次检查前 5 个帖子

            while not self.is_stopped: # 改为检查停止标志的无限循环
                # 在循环开始时检查是否已达到目标评论数
                if commented_count >= max_posts:
                    self._log(f"已成功评论 {commented_count} 个帖子，达到目标 {max_posts}，任务完成。")
                    break # 跳出循环

                self._log(f"--正在刷新并排序到最新 ---")
                post_processed_in_this_cycle = False # 标记本轮是否成功处理了一个帖子

                if not self._sort_by_newest():
                    self._log("刷新排序失败，将尝试处理当前页面内容。")
                    self.random_delay(1, 3) # 排序失败后等待
                # --- 结束刷新排序 ---

                # 重新获取当前页面上的帖子元素
                try:
                    current_posts = self.page.eles(post_item_selector, timeout=10) # 增加超时
                    if not current_posts:
                        self._log("刷新后当前页面仍未找到任何帖子元素，可能没有新帖子或加载失败，稍后重试...")
                        self.random_delay(10, 15) # 等待一段时间再尝试刷新
                        # self.random_delay(1, 2) # 此处已有较长等待，可酌情添加或不加
                        continue # 继续下一次 while 循环
                    self._log(f"刷新后当前页面找到 {len(current_posts)} 个帖子元素。")
                except Exception as find_err:
                    self._log(f"刷新后查找帖子元素时出错: {find_err}，结束任务。")
                    break

                # 查找并处理前 posts_to_check 个帖子中的第一个未处理的帖子
                for i, post_item in enumerate(current_posts[:posts_to_check]): # 只检查前 posts_to_check 个
                    # 提取帖子的唯一链接 (href)
                    post_href = None
                    try:
                        link_element = post_item.ele('tag:a', timeout=1)
                        if link_element:
                            href_value = link_element.attr('href')
                            if href_value and '/explore/' in href_value:
                                post_href = href_value
                        else:
                            self._log(f"当前列表第 {i+1} 个帖子未找到内部链接元素。")
                            continue
                    except Exception as e:
                        self._log(f"提取当前列表第 {i+1} 个帖子链接时出错: {e}。")
                        continue

                    if not post_href:
                        self._log(f"当前列表第 {i+1} 个帖子未能获取有效链接。")
                        continue

                    # 检查此帖子是否已处理
                    if post_href in processed_hrefs:
                        self._log(f"当前列表第 {i+1} 个帖子 (href: {post_href}) 已处理过，跳过。")
                        self.random_delay(0.5, 1.5) # 跳过已处理帖子
                        continue

                    # 找到一个未处理的帖子
                    self._log(f"找到未处理帖子: 当前列表第 {i+1} 个 (href: {post_href})，准备处理...")

                    # 点击帖子进入详情页
                    try:
                        self.random_delay(1, 3) # 点击帖子前等待
                        post_item.click()
                        self.page.wait.doc_loaded(timeout=10) # 等待文档加载完成
                        self.random_delay(2, 4) # 帖子加载后等待
                    except Exception as click_err:
                        self._log(f"点击帖子 (href: {post_href}) 时出错: {click_err}，跳过此帖子。")
                        self.random_delay(1, 2) # 点击出错后等待
                        continue # 处理下一个帖子

                    # --- 开始评论 ---
                    try:
                        self._log("尝试定位评论区并评论...")
                        current_tab = self.page
                        # 尝试多种可能的评论输入框提示文本
                        comment_input_area = None
                        for prompt in ['说点什么', '评论']:
                            try:
                                comment_input_area = current_tab.ele(f'text:{prompt}', timeout=5)
                                if comment_input_area:
                                    break
                            except ElementNotFoundError:
                                continue
                        
                        if not comment_input_area:
                            raise ElementNotFoundError("未能通过文本 '说点什么'或'评论'找到评论输入区域")

                        comment_text = random.choice(comment_texts)
                        self._log(f"准备输入评论: {comment_text}")
                        self.random_delay(1, 3) # 输入评论前等待
                        comment_input_area.input(comment_text)
                        self.random_delay(1, 2) # 输入后、点击发送前等待 (原已有)

                        # 定位并点击发送按钮
                        send_button = current_tab.ele('css:.submit', timeout=5)
                        if not send_button:
                            raise ElementNotFoundError("未能找到发送按钮")

                        self.random_delay(0.5, 1.5) # 点击发送前短等待
                        send_button.click()
                        self._log("评论已发送")
                        commented_count += 1 # 评论成功，计数器加一
                        post_processed_in_this_cycle = True # 标记本轮成功处理
                        processed_hrefs.add(post_href) # 评论成功后才标记为已处理
                        self._save_processed_hrefs(processed_hrefs) # 评论成功后保存
                        self._log(f"评论成功，将 {post_href} 标记为已处理并保存。当前已评论 {commented_count}/{max_posts} 个帖子。")
                        self.random_delay(5, 30) # 评论后等待更长时间

                        self._log("评论成功，尝试点击关闭按钮 .close-circle ...")
                        try:
                            close_button = self.page.ele(".close-circle", timeout=3) # 尝试定位关闭按钮
                            if close_button:
                                close_button.click(by_js=True) # 使用JS点击以增加成功率
                                self._log("已点击关闭按钮 (.close-circle)。")
                                self.random_delay(2, 4) # 等待弹窗关闭
                            else:
                                self._log("未找到关闭按钮 (.close-circle)，可能帖子页面结构已变化或弹窗自动关闭。将等待一段时间让页面稳定。")
                                self.random_delay(2, 4) # 等待页面稳定
                        except Exception as e_close:
                            self._log(f"点击关闭按钮 (.close-circle) 时出错: {e_close}。将等待一段时间让页面稳定。")
                            self.random_delay(2, 4) # 等待页面稳定
                        self.page.wait.doc_loaded(timeout=10) # 等待页面加载稳定
                    except ElementNotFoundError as find_err:
                        self._log(f"评论失败：未能找到元素 - {find_err}")
                        # 即使评论失败（例如找不到评论框或发送按钮），也应将帖子标记为已处理，避免重复尝试
                        processed_hrefs.add(post_href)
                        self._save_processed_hrefs(processed_hrefs)
                        self._log(f"评论失败（未找到元素），但将 {post_href} 标记为已处理并保存。")
                        self.random_delay(2, 4) # 评论失败（未找到元素）后等待
                    except Exception as comment_err:
                        self._log(f"评论过程中发生错误: {comment_err}")
                        traceback.print_exc()
                        processed_hrefs.add(post_href)
                        self._save_processed_hrefs(processed_hrefs)
                        self._log(f"评论失败（异常），但将 {post_href} 标记为已处理。")
                        self.random_delay(2, 4) # 评论失败（其他异常）后等待
                    # --- 结束评论 ---

                    # 评论一次后立即跳出 for 循环，进入下一轮 while
                    break

                # --- 内层 for 循环结束 ---

                # 如果在本轮查找中没有处理任何帖子（即遍历完前 posts_to_check 个帖子都是已处理或出错跳过）
                if not post_processed_in_this_cycle:
                    self._log(f"前 {posts_to_check} 个帖子均已处理或无法评论，等待一段时间后重试...")
                    self.random_delay(15, 25) # 等待更长时间
                    # 这里不需要 continue，循环会自动继续

            # 循环结束后的日志（可能是因为达到目标或被停止）
            if self.is_stopped:
                self._log("任务被外部停止。")
            else:
                 self._log("评论任务正常结束。")

            return commented_count >= max_posts # 返回是否达到了目标评论数

        except Exception as e:
            self._log(f"搜索和评论过程中发生错误: {e}")
            traceback.print_exc()
            return False

